package com.xjd.edu.toolkit.helper;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;

import javax.validation.constraints.NotNull;
import java.time.Duration;
import java.util.concurrent.Callable;

/**
 * 重试执行器
 * <p>
 * 系统中存在一个开源 Retry 库，但比较旧了，而且使用稍微不太方便（可能是为了通用性设计的稍显复杂），
 * 重点是不是Guava官方维护的，且2015年后就已不再维护了，故不再使用（这里加以说明的原因是部分工程在使用这个库）
 * <p>
 * https://github.com/rholder/guava-retrying
 *
 * @author wangyu
 * @created 2025/3/15 11:20
 */
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class RetryExecutor {

    /**
     * 重试（带返回值）
     *
     * @param retries   重试次数[1, 5]
     * @param waitTime  重试间隔时间
     * @param callable  重试闭包函数
     * @param predicate 重试断言，符合断言则进行重试（注：断言参数皆可能为空）
     * @param <T>
     * @return
     * @throws Exception
     */
    public static <T> T call(int retries, Duration waitTime, @NotNull Callable<T> callable, @NotNull RetryPredicate<T> predicate) throws Exception {
        Assert.isTrue(retries > 0 && retries <= 5, "重试次数必须大于0，且不能大于5");
        Assert.notNull(waitTime, "重试等待时间不能为空");
        Exception ex = null;
        T result = null;
        for (int i = 1; i <= retries; i++) {
            // 休眠指定时间
            Thread.sleep(waitTime.toMillis());
            log.warn("重试第 {} 次", i);
            // 执行重试逻辑
            try {
                result = callable.call();
                if (predicate.test(result)) {
                    // 不符合重试条件，直接返回
                    return result;
                } else {
                    // 如果重试执行成功得到结果，需要把前次执行异常置空
                    ex = null;
                }
            } catch (Exception e) {
                ex = e;
                if (i > 0) {
                    log.error("重试失败 {} 次", i, e);
                }
                // 不符合条件则终止重试
                if (!predicate.test(e)) {
                    break;
                }
            }
        }
        if (ex != null) {
            throw ex;
        }
        return result;
    }

    /**
     * 重试（带返回值），重试条件为任意异常
     *
     * @param retries  重试次数[1, 5]
     * @param waitTime 重试间隔时间
     * @param callable 重试闭包函数
     * @param <T>
     * @return
     * @throws Exception
     */
    public static <T> T call(int retries, Duration waitTime, @NotNull Callable<T> callable) throws Exception {
        return call(retries, waitTime, callable, (ex, result) -> ex != null);
    }

    /**
     * 重试（不带返回值）
     *
     * @param retries   重试次数[1, 5]
     * @param waitTime  重试间隔时间
     * @param runnable  重试闭包函数
     * @param predicate 重试断言，符合断言则进行重试（注：断言参数皆可能为空）
     * @throws Exception
     */
    public static void run(int retries, Duration waitTime, @NotNull Runnable runnable, @NotNull RetryPredicate<Object> predicate) throws Exception {
        Assert.isTrue(retries > 0 && retries <= 5, "重试次数必须大于0，且不能大于5");
        Assert.notNull(waitTime, "重试等待时间不能为空");
        Exception ex = null;
        for (int i = 1; i <= retries; i++) {
            // 休眠指定时间
            Thread.sleep(waitTime.toMillis());
            log.warn("重试第 {} 次", i);
            // 执行重试逻辑
            try {
                runnable.run();
                // 成功执行后要将前次异常置空
                ex = null;
                break;
            } catch (Exception e) {
                ex = e;
                if (i > 0) {
                    log.error("重试失败 {} 次", i, e);
                }
                // 不符合条件则终止重试
                if (!predicate.test(e)) {
                    break;
                }
            }
        }
        if (ex != null) {
            throw ex;
        }
    }

    /**
     * 重试（不带返回值），重试条件为任意异常
     *
     * @param retries  重试次数[1, 5]
     * @param waitTime 重试间隔时间
     * @param runnable 重试闭包函数
     * @throws Exception
     */
    public static void run(int retries, Duration waitTime, @NotNull Runnable runnable) throws Exception {
        run(retries, waitTime, runnable, (ex, none) -> ex != null);
    }

    /**
     * 重试断言闭包
     *
     * @param <T>
     */
    @FunctionalInterface
    public interface RetryPredicate<T> {

        /**
         * 判断是否符合重试条件
         *
         * @param ex
         * @param result
         * @return
         */
        boolean test(Exception ex, T result);

        default boolean test(Exception exception) {
            return test(exception, null);
        }

        default boolean test(T result) {
            return test(null, result);
        }
    }

}
